package require

import (
	"bufio"
	"bytes"
	"fmt"
	"gitee.com/ymofen/gotest"
	"reflect"
	"runtime"
	"strings"
	"unicode"
	"unicode/utf8"
)

type labeledContent struct {
	label   string
	content string
}

// isNil checks if a specified object is nil or not, without Failing.
func isNil(object interface{}) bool {
	if object == nil {
		return true
	}

	value := reflect.ValueOf(object)
	switch value.Kind() {
	case
		reflect.Chan, reflect.Func,
		reflect.Interface, reflect.Map,
		reflect.Ptr, reflect.Slice, reflect.UnsafePointer:

		return value.IsNil()
	}

	return false
}

func fail(t gotest.TestingT, msgAndArgs ...interface{}) bool {
	if h, ok := t.(gotest.TestHelper); ok {
		h.Helper()
	}
	content := []labeledContent{
		{"Caller", strings.Join(CallerInfo(), "\n\t\t\t")},
	}

	message := messageFromMsgAndArgs(msgAndArgs...)
	if len(message) > 0 {
		content = append(content, labeledContent{"Messages", message})
	}

	t.Errorf("\n%s", ""+labeledOutput(content...))

	return false
}

func assert(t gotest.TestingT, b bool, msgAndArgs ...interface{}) bool {
	if b {
		return true
	}
	if h, ok := t.(gotest.TestHelper); ok {
		h.Helper()
	}
	content := []labeledContent{
		{"Caller", strings.Join(CallerInfo(), "\n\t\t\t")},
	}

	message := messageFromMsgAndArgs(msgAndArgs...)
	if len(message) > 0 {
		content = append(content, labeledContent{"Messages", message})
	}

	t.Errorf("\n%s", ""+labeledOutput(content...))

	return false
}

func True(t gotest.TestingT, b bool, msgAndArgs ...interface{}) {
	if h, ok := t.(gotest.TestHelper); ok {
		h.Helper()
	}
	if assert(t, b, msgAndArgs...) {
		return
	}
	t.FailNow()
}

func Equal(t gotest.TestingT, a, b any, msgAndArgs ...interface{}) {
	if h, ok := t.(gotest.TestHelper); ok {
		h.Helper()
	}
	if assert(t, a == b, msgAndArgs...) {
		return
	}
	t.FailNow()
}

func NotEqual(t gotest.TestingT, a, b any, msgAndArgs ...interface{}) {
	if h, ok := t.(gotest.TestHelper); ok {
		h.Helper()
	}
	if assert(t, a != b, msgAndArgs...) {
		return
	}
	t.FailNow()
}

func Nil(t gotest.TestingT, a any, msgAndArgs ...interface{}) {
	if !isNil(a) { // 有错误
		if h, ok := t.(gotest.TestHelper); ok {
			h.Helper()
		}

		if len(msgAndArgs) == 0 {
			fail(t, fmt.Sprintf("Expected nil, but got: %#v", a))
		} else {
			fail(t, msgAndArgs...)
		}
	}
}

func NotNil(t gotest.TestingT, a any, msgAndArgs ...interface{}) {
	if isNil(a) { // 没错误
		if h, ok := t.(gotest.TestHelper); ok {
			h.Helper()
		}
		if len(msgAndArgs) == 0 {
			fail(t, "Expected value not to be nil.")
		} else {
			fail(t, msgAndArgs...)
		}
	}
}

func NoError(t gotest.TestingT, err error, msgAndArgs ...interface{}) {
	if !isNil(err) { // 有错误
		if h, ok := t.(gotest.TestHelper); ok {
			h.Helper()
		}

		if len(msgAndArgs) == 0 {
			fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err))
		} else {
			fail(t, msgAndArgs...)
		}
	}
	return
}

func Error(t gotest.TestingT, err error, msgAndArgs ...interface{}) bool {
	if isNil(err) { // 没错误
		if h, ok := t.(gotest.TestHelper); ok {
			h.Helper()
		}
		if len(msgAndArgs) == 0 {
			fail(t, "An error is expected but got nil.")
		} else {
			fail(t, msgAndArgs...)
		}
	}

	return true
}

func CallerInfo() []string {

	var pc uintptr
	var ok bool
	var file string
	var line int
	var name string

	callers := []string{}
	for i := 0; ; i++ {
		pc, file, line, ok = runtime.Caller(i)
		if !ok {
			// The breaks below failed to terminate the loop, and we ran off the
			// end of the call stack.
			break
		}

		// This is a huge edge case, but it will panic if this is the case, see #180
		if file == "<autogenerated>" {
			break
		}

		f := runtime.FuncForPC(pc)
		if f == nil {
			break
		}
		name = f.Name()

		// testing.tRunner is the standard library function that calls
		// tests. Subtests are called directly by tRunner, without going through
		// the Test/Benchmark/Example function that contains the t.Run calls, so
		// with subtests we should break when we hit tRunner, without adding it
		// to the list of callers.
		if name == "testing.tRunner" {
			break
		}

		parts := strings.Split(file, "/")
		if len(parts) > 1 {
			filename := parts[len(parts)-1]
			dir := parts[len(parts)-2]
			if (dir != "assert" && dir != "mock" && dir != "require") || filename == "mock_test.go" {
				callers = append(callers, fmt.Sprintf("%s:%d", file, line))
			}
		}

		// Drop the package
		segments := strings.Split(name, ".")
		name = segments[len(segments)-1]
		if isTest(name, "Test") ||
			isTest(name, "Benchmark") ||
			isTest(name, "Example") {
			break
		}
	}

	return callers
}

func isTest(name, prefix string) bool {
	if !strings.HasPrefix(name, prefix) {
		return false
	}
	if len(name) == len(prefix) { // "Test" is ok
		return true
	}
	r, _ := utf8.DecodeRuneInString(name[len(prefix):])
	return !unicode.IsLower(r)
}

func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
	if len(msgAndArgs) == 0 || msgAndArgs == nil {
		return ""
	}
	if len(msgAndArgs) == 1 {
		msg := msgAndArgs[0]
		if msgAsStr, ok := msg.(string); ok {
			return msgAsStr
		}
		return fmt.Sprintf("%+v", msg)
	}
	if len(msgAndArgs) > 1 {
		return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
	}
	return ""
}

func labeledOutput(content ...labeledContent) string {
	longestLabel := 0
	for _, v := range content {
		if len(v.label) > longestLabel {
			longestLabel = len(v.label)
		}
	}
	var output string
	for _, v := range content {
		output += "\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n"
	}
	return output
}

func indentMessageLines(message string, longestLabelLen int) string {
	outBuf := new(bytes.Buffer)

	for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ {
		// no need to align first line because it starts at the correct location (after the label)
		if i != 0 {
			// append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab
			outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t")
		}
		outBuf.WriteString(scanner.Text())
	}

	return outBuf.String()
}
